eletrotupi / tcc/ frontend/app/entry/[id].tsx4f91bfff4faf05b737920b87ee6ce8108a2eef35
13.3 KB | 439 lines Raw
1
import { useState, useEffect, use } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { format } from "date-fns";
12
import { ptBR } from "date-fns/locale";
13

14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
16
import { Badge } from "@/components/ui/Badge";
17
import { Button } from "@/components/ui/Button";
18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
19
import { Section, SectionHeader } from "@/components/ui/Sections";
20
import { Theme, Colors, Spacing } from "@/constants/theme";
21
import { getMood } from "@/constants/moods";
22
import {
23
  getMoodComponent,
24
  intensityLabel,
25
  intensityToValue,
26
} from "@/constants/mood-components";
27
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
28
import { getTimeBadge, formatMoment } from "@/lib/utils/time";
29
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
30
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
31
import { getTriggerCategory } from "@/constants/trigger-categories";
32
import { getCareActionMeta } from "@/constants/care-action-categories";
33

34
const impactColor = (n: number) => {
35
  if (n <= 1) return '#86efac';
36
  if (n <= 2) return '#22c55e';
37
  if (n <= 3) return '#f59e0b';
38
  if (n <= 4) return '#f97316';
39
  return '#ef4444';
40
};
41

42
export default function MoodDetailScreen() {
43
  const { id } = useLocalSearchParams<{ id: string }>();
44
  const { data: entry, isLoading, isError } = useMoodEntry(id);
45
  const deleteMoodEntry = useDeleteMoodEntry();
46
  const [minutesLeft, setMinutesLeft] = useState(0);
47
  const [canDelete, setCanDelete] = useState(false);
48

49
  useEffect(() => {
50
    const updateTimer = () => {
51
      const moment = new Date(entry?.moment);
52
      const createdAt = new Date(entry?.moment);
53
      const msSinceCreation = Date.now() - createdAt.getTime();
54
      const deletionWindowMs = 5 * 60 * 1000;
55

56
      const newCanDelete = msSinceCreation < deletionWindowMs;
57
      const newMinutesLeft = Math.max(
58
        0,
59
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
60
      );
61

62
      setCanDelete(newCanDelete);
63
      setMinutesLeft(newMinutesLeft);
64
    };
65

66
    updateTimer(); // Set initial values
67

68
    const interval = setInterval(updateTimer, 1000);
69

70
    return () => clearInterval(interval);
71
  }, [entry?.moment]);
72

73
  if (isLoading) {
74
    return (
75
      <View style={styles.centered}>
76
        <ActivityIndicator size="large" color={Theme.colors.light.tint} />
77
      </View>
78
    );
79
  }
80

81
  if (isError || !entry) {
82
    return (
83
      <View style={styles.centered}>
84
        <Text style={styles.errorText}>Registro não encontrado.</Text>
85
        <Pressable onPress={() => router.back()} style={styles.backButton}>
86
          <Text style={styles.backButtonText}>Voltar</Text>
87
        </Pressable>
88
      </View>
89
    );
90
  }
91

92
  const moment = new Date(entry.moment);
93
  const moodDef = getMood(entry.selectedMood.toLowerCase());
94
  const badge = getTimeBadge(moment);
95

96
  const handleDelete = async () => {
97
    await deleteMoodEntry.mutateAsync(String(entry.id));
98

99
    router.back();
100
  };
101

102
  const deleteButtonMsg = () => {
103
    const msg = "Excluir Registro";
104
    const timeLeftStr = `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
105
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
106

107
    return `${msg} ${suffix}`;
108
  };
109

110
  const hasAnyLinks = (
111
    entry.triggerLinks && entry.triggerLinks.length > 0
112
  ) || (entry.careActions && entry.careActions.length > 0);
113

114
  return (
115
    <>
116
      <Stack.Screen options={{ title: "Ver detalhes" }} />
117

118
      <ScrollView
119
        style={styles.container}
120
        contentContainerStyle={styles.content}
121
        showsVerticalScrollIndicator={false}
122
      >
123
        {/* Header card */}
124
        <Card style={styles.headerCard}>
125
          <Between>
126
            <Row gap={12} style={{ alignItems: "center" }}>
127
              <Text style={styles.moodIcon}>{moodDef?.icon ?? "😐"}</Text>
128
              <Col gap={2}>
129
                <Text style={styles.moodLabel}>
130
                  {moodDef?.label ?? entry.selectedMood}
131
                </Text>
132
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
133
              </Col>
134
            </Row>
135
            {/* If this is the latest mood entry, show the active badge */}
136
            <Badge label="Registro Ativo" variant="green" />
137
          </Between>
138
        </Card>
139

140
        {/* Core levels */}
141
        <Section>
142
          <SectionHeader title="Níveis emocionais" variant="subtle" />
143
          {[
144
            { id: "energy", label: "Energia", value: entry.energyLevel },
145
            { id: "stress", label: "Estresse", value: entry.stressLevel },
146
            { id: "anxiety", label: "Ansiedade", value: entry.anxietyLevel },
147
          ].map((item, index, arr) => (
148
            <Card key={item.id} style={{ padding: Spacing.cardGap }}>
149
              <View key={item.label}>
150
                <View style={styles.componentRow}>
151
                  <Text style={styles.componentLabel}>{item.label}</Text>
152
                  <Text style={styles.componentIntensityGreen}>
153
                    {item.value}/10
154
                  </Text>
155
                </View>
156

157
                <View style={styles.progressTrack}>
158
                  <View
159
                    style={[
160
                      styles.progressFill,
161
                      { width: `${(item.value / 10) * 100}%` },
162
                    ]}
163
                  />
164
                </View>
165
              </View>
166
            </Card>
167
          ))}
168
        </Section>
169

170
        <Section>
171
          <SectionHeader title="Além disso, sinto..." variant="subtle" />
172
          <Col gap={0}>
173
            {entry.moodComponents.map((comp) => {
174
              return (
175
                <MoodComponentCard
176
                  key={comp.id}
177
                  id={comp.component.toLowerCase()}
178
                  intensity={comp.intensity}
179
                />
180
              );
181
            })}
182
          </Col>
183
        </Section>
184

185
        {/* Linked triggers */}
186
        <Section>
187
          <SectionHeader title="Vínculo" variant="subtle" />
188
          {!!hasAnyLinks ? (
189
            <Col gap={8}>
190
              {entry.triggerLinks.map((link) => {
191
                const cat = getTriggerCategory(link.trigger.category);
192
                return (
193
                  <Pressable
194
                    key={link.id}
195
                    onPress={() => router.push(`/triggers/${link.trigger.id}`)}
196
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
197
                  >
198
                    <Card style={styles.linkCard}>
199
                      <Row style={{ alignItems: "center", gap: 12 }}>
200
                        <View style={[styles.linkIconCircle, { backgroundColor: cat.iconBackground }]}>
201
                          <MaterialIcons name={cat.icon} size={22} color={cat.color} />
202
                        </View>
203
                        <Col gap={3} style={{ flex: 1 }}>
204
                          <Row style={{ alignItems: "center", gap: 6 }}>
205
                            <Text style={styles.linkLabel}>{cat.label}</Text>
206
                            <Badge label="Gatilho" variant="orange" style={undefined} />
207
                          </Row>
208
                          <Text style={styles.linkTime}>
209
                            {formatMoment(new Date(link.trigger.moment))}
210
                          </Text>
211
                        </Col>
212
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
213
                      </Row>
214
                      <View style={styles.impactBar}>
215
                        <View
216
                          style={[
217
                            styles.impactFill,
218
                            {
219
                              width: `${(link.perceivedImpact / 5) * 100}%`,
220
                              backgroundColor: impactColor(link.perceivedImpact),
221
                            },
222
                          ]}
223
                        />
224
                      </View>
225
                    </Card>
226
                  </Pressable>
227
                );
228
              })}
229

230
              {(entry.careActions ?? []).map((ca) => {
231
                const meta = getCareActionMeta(ca);
232

233
                return (
234
                  <Pressable
235
                    key={ca.id}
236
                    onPress={() => router.push(`/care-actions/${ca.id}`)}
237
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
238
                  >
239
                    <Card style={styles.linkCard}>
240
                      <Row style={{ alignItems: 'center', gap: 12 }}>
241
                        <View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
242
                          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
243
                        </View>
244
                        <Col gap={3} style={{ flex: 1 }}>
245
                          <Row style={{ alignItems: 'center', gap: 6 }}>
246
                            <Text style={styles.linkLabel}>{meta.label}</Text>
247
                            <Badge label="Consulta" variant="blue" style={undefined} />
248
                          </Row>
249
                          <Text style={styles.linkTime}>
250
                            {formatMoment(new Date(ca.moment))}
251
                          </Text>
252
                        </Col>
253

254
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
255
                      </Row>
256
                    </Card>
257
                  </Pressable>
258
                );
259
              })}
260
            </Col>
261
          ) : (
262
            <NoLinkMessage />
263
          )}
264
        </Section>
265

266
        {/* Annotation / notes */}
267
        {entry.annotation && (
268
          <Section>
269
            <SectionHeader title="Anotações" variant="subtle" />
270
            <Card style={{ padding: Spacing.cardGap }}>
271
              <Text style={styles.annotation}>
272
                &quot;{entry.annotation?.trim()}&quot;
273
              </Text>
274
            </Card>
275
          </Section>
276
        )}
277

278
        {/* Delete */}
279
        <View style={styles.section}>
280
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
281
          <Button
282
            variant="danger"
283
            loading={deleteMoodEntry.isPending}
284
            disabled={!canDelete || deleteMoodEntry.isPending}
285
            onPress={handleDelete}
286
            title={deleteButtonMsg()}
287
          />
288

289
          <SubtleInfoCard
290
            style={styles.deleteInfo}
291
            text={`
292
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
293
              após o envio para garantir a integridade do histórico emocional.
294
              `}
295
          />
296
        </View>
297
      </ScrollView>
298
    </>
299
  );
300
}
301

302
const styles = StyleSheet.create({
303
  container: {
304
    flex: 1,
305
    backgroundColor: Theme.colors.light.background,
306
  },
307
  content: {
308
    padding: Spacing.containerPadding,
309
    gap: Spacing.sectionGap,
310
    paddingBottom: 48,
311
  },
312
  centered: {
313
    flex: 1,
314
    justifyContent: "center",
315
    alignItems: "center",
316
    gap: 12,
317
  },
318
  errorText: {
319
    fontSize: 15,
320
    color: Colors.light.textSecondary,
321
  },
322
  backButton: {
323
    paddingHorizontal: 20,
324
    paddingVertical: 10,
325
    backgroundColor: Theme.colors.light.tint,
326
    borderRadius: Theme.borderRadius.md,
327
  },
328
  backButtonText: {
329
    fontWeight: "700",
330
    color: Theme.colors.light.text,
331
  },
332

333
  // Header
334
  headerCard: {
335
    padding: 16,
336
  },
337
  moodIcon: {
338
    fontSize: 40,
339
  },
340
  moodLabel: {
341
    fontSize: 17,
342
    fontWeight: "700",
343
    color: Colors.light.text,
344
  },
345
  moodTime: {
346
    fontSize: 13,
347
    color: Colors.light.textSecondary,
348
  },
349

350
  // Sections
351
  section: {
352
    gap: 8,
353
  },
354
  sectionLabel: {
355
    fontSize: 11,
356
    fontWeight: "600",
357
    letterSpacing: 0.8,
358
    color: Colors.light.textSecondary,
359
    textTransform: "uppercase",
360
  },
361

362
  // Components
363
  componentRow: {
364
    flexDirection: "row",
365
    justifyContent: "space-between",
366
    alignItems: "center",
367
    paddingVertical: 10,
368
  },
369
  componentLabel: {
370
    fontSize: 15,
371
    fontWeight: "700",
372
    color: Colors.light.text,
373
  },
374
  componentIntensity: {
375
    fontSize: 13,
376
    color: Colors.light.textSecondary,
377
  },
378
  componentIntensityGreen: {
379
    fontSize: 13,
380
    fontWeight: "700",
381
    color: Theme.colors.light.tint,
382
  },
383
  progressTrack: {
384
    height: 6,
385
    backgroundColor: Theme.colors.light.divider,
386
    borderRadius: Theme.borderRadius.full,
387
    marginBottom: 4,
388
    overflow: "hidden",
389
  },
390
  progressFill: {
391
    height: "100%",
392
    backgroundColor: Theme.colors.light.tint,
393
    borderRadius: Theme.borderRadius.full,
394
  },
395
  // Annotation
396
  annotation: {
397
    fontSize: 14,
398
    lineHeight: 22,
399
    color: Colors.light.textSecondary,
400
    fontStyle: "italic",
401
    padding: 4,
402
  },
403
  deleteInfo: {
404
    marginTop: 4,
405
  },
406
  // Linked trigger cards
407
  linkCard: {
408
    padding: Spacing.cardGap,
409
    overflow: "hidden",
410
    gap: 10,
411
  },
412
  linkIconCircle: {
413
    width: 44,
414
    height: 44,
415
    borderRadius: 22,
416
    alignItems: "center",
417
    justifyContent: "center",
418
  },
419
  linkTime: {
420
    fontSize: 12,
421
    color: Colors.light.textSecondary,
422
  },
423
  linkLabel: {
424
    fontSize: 15,
425
    fontWeight: "700",
426
    color: Colors.light.text,
427
  },
428
  impactBar: {
429
    height: 3,
430
    backgroundColor: Colors.light.divider,
431
    borderRadius: 99,
432
    overflow: "hidden",
433
  },
434
  impactFill: {
435
    height: "100%",
436
    borderRadius: 99,
437
  },
438

439
});